package com.uxsino.simo.indicator;

import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;

import com.google.common.base.Strings;
import org.apache.commons.lang3.StringEscapeUtils;
import org.apache.commons.lang3.StringUtils;

import com.alibaba.fastjson.annotation.JSONField;
import com.fasterxml.jackson.annotation.JsonAnyGetter;
import com.fasterxml.jackson.annotation.JsonAnySetter;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.uxsino.commons.logicSelector.PropProxy;
import com.uxsino.commons.utils.config.ConfigProp;
import com.uxsino.commons.utils.config.PropElement;
import com.uxsino.reactorq.model.FieldType;
import com.uxsino.reactorq.model.INDICATOR_TYPE;
import com.uxsino.simo.networkentity.EntityInfo;
import com.uxsino.simo.query.QueryContext;
import com.uxsino.simo.utils.ConfigLoadingContext;

public abstract class Indicator {
    public String name;

    @ConfigProp(name = "label")
    public String label;

    @ConfigProp(name = "nonexec")
    public String nonexec;

    @ConfigProp(name = "withoutrule")
    public String withoutrule;

    @ConfigProp(name = "incremental")
    public String incremental;

    @ConfigProp(name = "neclass")
    public String neclass;

    @ConfigProp(name = "versions")
    public String versions;

    @ConfigProp(name = "component-name-formula")
    public String componentNameFormula;

    @ConfigProp(name = "componentType")
    public String componentType;

    @ConfigProp(name = "componentKey")
    public String componentKey;

    /**
     * 不配置协议的指标标注为符合所有协议的指标
     */
    @ConfigProp(name = "protocols")
    public String protocols;

    @ConfigProp(name = "vendorIds")
    public String vendorIds;

    @ConfigProp(name = "tag")
    public String tag;

    @ConfigProp(name = "no-key")
    public String noKey;

    public Set<String> warnGroups = new HashSet<>();

    private Map<String, String> properties;

    public Indicator(String indicatorName) {
        name = indicatorName;
        properties = new HashMap<>();
    }

    @JsonProperty("type")
    @JSONField(name = "type")
    public abstract INDICATOR_TYPE getIndicatorType();

    public static INDICATOR_TYPE getIndTypeByName(String typeName) {
        switch (typeName) {
        case "string":
            return INDICATOR_TYPE.STRING;
        case "number":
            return INDICATOR_TYPE.NUMBER;
        case "datetime":
            return INDICATOR_TYPE.DATETIME;
        case "compound":
            return INDICATOR_TYPE.COMPOUND;
        case "list":
            return INDICATOR_TYPE.LIST;
        case "percent":
        case "%":
            return INDICATOR_TYPE.PERCENT;
        case "boolean":
            return INDICATOR_TYPE.BOOLEAN;
        }

        return INDICATOR_TYPE.UNKNOWN;
    }

    // override hash code for maps
    @Override
    public final int hashCode() {
        return name.hashCode();
    }

    @Override
    public final boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null || !(obj instanceof Indicator)) {
            return false;
        }
        return this.name.equals(((Indicator) obj).name);
    }

    @JsonAnyGetter
    private Map<String, String> getProperties() {
        return properties;
    }

    @PropProxy
    public String getProperty(String propName) {
        return properties.get(propName);
    }

    @JsonAnySetter
    public void setProperty(String propName, String value) {
        properties.put(propName, value);
    }

    public Object parse(String stringValue) {
        return parse(stringValue, getIndicatorType(), null);
    }

    // //////////////////////////// static members ///////////////////////////////////////

    private static final Pattern variableNamePattern = Pattern.compile("[a-zA-Z_][a-zA-Z0-9_]*");

    public static boolean checkVariableName(String name) {
        return variableNamePattern.matcher(name).matches();

    }

    /**
     * action to take when query is finished
     * @param entity
     * @param ctxt
     */
    public abstract void doPostQuery(EntityInfo entity, QueryContext ctxt, Object value);

    /**
     * parse simple value (string number percent) from string
     * @param stringValue
     * @param type
     * @param format
     * @return
     */
    public static Object parse(String stringValue, INDICATOR_TYPE type, String format) {
        if (stringValue == null) {
            return null;
        }
        switch (type) {
        case STRING:
            return stringValue;
        case PERCENT:
        case NUMBER:
            return Double.parseDouble(stringValue);
        default:
            throw new IllegalArgumentException("can not parse value from \'" + stringValue + "\' to " + type);
        }
    }

    public static FieldType getFieldTypeByName(String typeName, FieldType defaultType) {
        FieldType type = FieldType.createByName(typeName);
        if (type != null) {
            return type;
        }
        return defaultType;
    }

    public static Class<?> getClassForIndicatorType(INDICATOR_TYPE type) {
        switch (type) {
        case STRING:
        case DATETIME:	// TODO use string for now
            return String.class;
        case PERCENT:
        case NUMBER:
            return Double.class;
        case LIST:
            return List.class;
        case COMPOUND:
            return Map.class;
        default:
            return Void.class;

        }
    }

    @SuppressWarnings("unchecked")
    public static <T> T castValue(Object value, INDICATOR_TYPE type) {
        Class<?> cls = getClassForIndicatorType(type);
        if (value == null || cls.isAssignableFrom(value.getClass())) {
            return (T) value;
        }
        return (T) parse(value.toString(), type, "");
    }

    // public static <T extends Indicator> T create

    public static Indicator create(PropElement eIndicator, ConfigLoadingContext lctxt) {
        String indicatorName = eIndicator.getProp("name").trim();

        if (!checkVariableName(indicatorName)) {

            lctxt.error(eIndicator.getSourceLocation(), " illegal indicator name: {}", indicatorName);
        }
        Indicator indicator;
        switch (eIndicator.getTagName()) {
        case "indicator":
            indicator = createGeneralIndicator(indicatorName, eIndicator, lctxt);
            break;
        case "expr":
            indicator = createExprIndicator(indicatorName, eIndicator, lctxt);
            break;
        case "refer-set":
            indicator = createReferSetIndicator(indicatorName, eIndicator, lctxt);
            break;
        case "aggregate":
            indicator = createAggregateIndicator(indicatorName, eIndicator, lctxt);
            break;
        default:
            throw new IllegalArgumentException("unknown indicator type: " + eIndicator.getTagName());
        }
        // indicator.versions = eIndicator.getProp("versions");
        try {
            lctxt.getPropLoader().loadProperties(indicator, eIndicator);
        } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
            lctxt.error(eIndicator.getSourceLocation(), "error loading properties for indicator:{}", indicatorName, e);
            return null;
        }
        return indicator;
    }

    private static String getFormula(String name, PropElement element, ConfigLoadingContext lctxt) {
        String formula = element.getProp("formula");

        PropElement formulaElement = element.getFirstElement("formula");

        if (formulaElement != null) {
            String sNewFormula = formulaElement.getText();
            if (StringUtils.isNotEmpty(formula) && StringUtils.isNotEmpty(sNewFormula)) {
                lctxt.error(element.getSourceLocation(),
                    "{} formula duplicated in attribute and element, using attribute.", name);
            } else {
                formula = sNewFormula;
            }
        }
        return formula;
    }

    private static Indicator createExprIndicator(String indicatorName, PropElement eIndicator,
        ConfigLoadingContext lctxt) {

        String indicatorTypeName = eIndicator.getProp("type").trim();

        ExprIndicator result;
        String formula = null;

        formula = StringEscapeUtils.unescapeJava(getFormula(indicatorName, eIndicator, lctxt));

        if (StringUtils.isEmpty(formula)) {
            lctxt.error(eIndicator.getSourceLocation(), "formula is empty. {}", indicatorName);
        }
        result = new ExprIndicator(indicatorName, Indicator.getIndTypeByName(indicatorTypeName), formula);
        try {
            lctxt.getEvaluator().compileExprItem(result);
        } catch (Exception e) {
            result.disable();
            lctxt.error(eIndicator.getSourceLocation(), "indicator {} error compiling formula.\n {}", indicatorName, e);
        }
        return result;
    }

    private static AggregateIndicator createAggregateIndicator(String indicatorName, PropElement eIndicator,
        ConfigLoadingContext lctxt) {

        AggregateIndicator result;

        result = new AggregateIndicator(indicatorName);

        result.loadProp(eIndicator, lctxt);
        return result;
    }

    private static Indicator createReferSetIndicator(String indicatorName, PropElement eIndicator,
        ConfigLoadingContext lctxt) {

        return new ReferSetIndicator(indicatorName);
    }

    private static Indicator createGeneralIndicator(String indicatorName, PropElement eIndicator,
        ConfigLoadingContext lctxt) {
        Indicator result = null;
        String indicatorTypeName = eIndicator.getProp("type").trim();

        switch (indicatorTypeName.toLowerCase()) {
        case "":
        case "string":
            result = new SimpleIndicator(indicatorName, INDICATOR_TYPE.STRING);
            break;
        case "number":
            result = new SimpleIndicator(indicatorName, INDICATOR_TYPE.NUMBER);
            break;
            case "percent":
            result = new SimpleIndicator(indicatorName, INDICATOR_TYPE.PERCENT);
            break;
        case "datetime":
            result = new SimpleIndicator(indicatorName, INDICATOR_TYPE.DATETIME);
            break;
        case "list":
            result = createListIndicator(indicatorName, eIndicator, lctxt);
            break;
        case "compound":
            result = createCompoundIndicator(indicatorName, eIndicator, lctxt);
            break;
        case "boolean":
            result = new SimpleIndicator(indicatorName, INDICATOR_TYPE.BOOLEAN);
            break;
        default:
            throw new IllegalArgumentException("unknown indicator type: " + indicatorTypeName);
        }

        String warnGroupStr = eIndicator.getProp("warn_groups");
        String[] groups = warnGroupStr.split(",");
        for (String s : groups) {
            result.warnGroups.add(s);
        }
        if (StringUtils.isNotEmpty(eIndicator.getProp("incremental"))) {
            result.incremental = eIndicator.getProp("incremental");
        }
        return result;
    }

    private static CompoundIndicator createCompoundIndicator(String indicatorName, PropElement eIndicator,
        ConfigLoadingContext lctxt) {
        CompoundIndicator ci = new CompoundIndicator(indicatorName);
        createFields(ci, eIndicator, lctxt);
        return ci;

    }

    private static ListIndicator createListIndicator(String indicatorName, PropElement eIndicator,
        ConfigLoadingContext lctxt) {
        ListIndicator li = new ListIndicator(indicatorName);
        createFields(li, eIndicator, lctxt);
        return li;

    }

    private static void createFields(CompoundIndicator ind, PropElement eIndicator, ConfigLoadingContext lctxt) {
        List<PropElement> nl = eIndicator.getElements("fields");
        if (nl.size() == 0) {
            lctxt.error(eIndicator.getSourceLocation(), "fields not defined for {} indicator:{}",
                ind.getIndicatorType(), ind.name);
            return;
        }
        for (PropElement node: nl){
            for (PropElement eField : node.getChildElements()) {
                String fieldName = eField.getProp("name").trim();
                String typeName = eField.getTagName().trim();
                String label = eField.getProp("label").trim();
                String unit = eField.getProp("unit");
                String withoutrule = Strings.nullToEmpty(eField.getProp("withoutrule")).trim();
                String formula = getFormula(fieldName, eField, lctxt);
                String useFormulaNoValueOnly = eField.getProp("use-formula-no-value-only");
                String tag = eField.getProp("tag");
                if (!checkVariableName(fieldName)) {
                    lctxt.error(eField.getSourceLocation(), "illegal field name: {}.{}", ind.name, fieldName);
                }
                IIndicatorField fld;
                if (!StringUtils.isEmpty(formula)) {
                    fld = new ExprField(ind, fieldName, Indicator.getFieldTypeByName(typeName, FieldType.STRING));
                    ((ExprField) fld).setFormula(formula);
                    ((ExprField) fld).useFormulaNoValueOnly = Boolean.parseBoolean(useFormulaNoValueOnly);
                    try {
                        lctxt.getEvaluator().compileExprItem((ExprField) fld);
                    } catch (Exception e) {
                        ((ExprField) fld).disable();
                        lctxt.error(eField.getSourceLocation(), "error compiling forumla. {} {}\n{}", ind.name, fieldName,
                                e);
                    }

                } else {
                    fld = ind.createField(fieldName, Indicator.getFieldTypeByName(typeName, FieldType.STRING));
                }
                if (label != null && StringUtils.isNoneBlank(label)) {
                    fld.setLabel(label);
                }
                if (unit != null && StringUtils.isNoneBlank(unit)) {
                    fld.setUnit(unit);
                }
                if (withoutrule != null && StringUtils.isNoneBlank(withoutrule)) {
                    fld.setWithoutrule(withoutrule);
                }
                if (tag != null && StringUtils.isNoneBlank(tag)) {
                    fld.setTag(tag);
                }
                fld.setDesc(createDesc(eField));
                if (ind.hasField(fieldName)) {
                    lctxt.error(eField.getSourceLocation(), "indicator {}: duplicate field name {}", ind.name, fieldName);
                }
                ind.addField(fieldName, fld);

            }
        }

        ind.getFieldsCollection().forEach(fld -> {
            if (fld instanceof ExprField) {
                ExprField efld = (ExprField) fld;
                efld.getReferredNames().forEach(name -> {
                    if (!"$index$".equals(name) && ind.getField(name) == null) {
                        lctxt.error(eIndicator.getSourceLocation(), "{}.{} referred field {} doesn't exist", ind.name,
                            fld.getName(), name);
                    }
                });
            }
        });
    }

    private static Map<String, String> createDesc(PropElement eField) {
        List<PropElement> descElements = eField.getChildElements("desc");
        if (descElements == null || descElements.isEmpty()) {
            return null;
        }
        Map<String, String> descMap = new HashMap<>();
        descElements.forEach(e -> {
            String value = e.getProp("value").trim();
            String text = e.getProp("text").trim();
            if (StringUtils.isBlank(value) || StringUtils.isBlank(text)) {
                return;
            }
            descMap.put(value, text);
        });
        if (descMap.isEmpty()) {
            return null;
        }
        return descMap;
    }
}
